/**
 * Утилиты для парсинга строки типа SVG Path Data {@link http://www.w3.org/TR/SVG/paths.html#PathData}
 * Это тип был расширен еще одним параметром: O (x1 y1 x2 y2 x y) — кривая Безье,
 * чья косательная должна быть зафиксирована с предыдущей
 * @author Sergey Chikuyonok (serge.che@gmail.com)
 * @copyright Art.Lebedev Studio (http://www.artlebedev.ru)
 * 
 * @include "/ictinus/src/ictinus.js"
 * @include "/ictinus/src/ictinus.drawItems.js"
 * @include "/ictinus/src/ictinus.Shape.js"
 * 
 */
ictinus.Shape.pathData = (function(){
	/**
	 * Обрезает пробелы в начале и в конце текста
	 * @param {String} text
	 * @return {String}
	 */
	function trim(text){
		return (text || '').replace( /^\s+|\s+$/g, '');
	}
	
	/**
	 * Точка
	 * @class
	 */
	function Point(x, y) {
		this.x = x;
		this.y = y;
	}
	
	/**
	 * Конвертирует кривую второго порядка в кривую третьего порядка.
	 * Используется для того, чтобы избавиться от кривых второго порядка 
	 * (quad curve), которые неправильно работают в Fx1.5
	 * @link https://developer.mozilla.org/en/Canvas_tutorial/Drawing_shapes
	 * @param {Number} x0 Начальная X-координата
	 * @param {Number} y0 Начальная Y-координата
	 * @param {Number} cpx X-координата контрольной точки
	 * @param {Number} cpy Y-координата контрольной точки
	 * @param {Number} x1 Конечная X-координата
	 * @param {Number} y1 Конечная Y-координата
	 * 
	 */
	function conv2to3(x0, y0, cpx, cpy, x1, y1) {
		var cp1x = x0 + 2/3 * (cpx - x0);
		var cp1y = y0 + 2/3 * (cpy - y0);
		
		return {
			cp1x: cp1x,
			cp1y: cp1y,
			cp2x: cp1x + (x1 - x0) / 3,
			cp2y: cp1y + (y1 - y0) / 3,
			x: x1,
			y: y1
		};
	}
	
	/**
	 * Пробразует команды рисования кривых SVG в команды рисования иктинуса
	 * @param {Array} svg_commands Команды рисования кривых из SVG (создаются в <code>ictinus.Shape.utils.parsePath</code>)
	 * @return {Array} Команды рисования иктинуса
	 */
	var transformCommands = (function() {
		// Абсолютные координаты последней распарсенной точки 
		var abs_x = 0, abs_y = 0;
		
		/**
		 * Последняя контрольная точка 
		 * (используется в <code>scurveto()</code> и <code>squadto()</code>)
		 * @type {Point}
		 */
		var last_cp;
		
		/** Текущий набор команд рисования иктинуса */
		var commands = [];
		
		/* 
		 * Команды рисования, которые непосредственно переводят вызовы 
		 * из SVG в ictinus
		 */
		
		/** @type {ictinus.drawItems.Item} Последняя добавленная в список команда */
		var last_command = null;
		
		/**
		 * Добавление комманды рисования иктинуса в список
		 * @param {ictinus.drawItems.Item} command
		 */
		function addCommand(command){
			if (last_command) {
				last_command.next(command)
			}
			
			command.prev(last_command);
			commands.push(command);
			last_command = command;
		}
		
		/**
		 * @param {String} command Команда
		 * @param {Array[]|String} arg_list Набор аргументов команды
		 * @param {Number} pos Порядковый номер команды в списке комманд
		 */
		function moveto(command, arg_list, pos) {
			arg_list = expandArgs(arg_list, 2);
			
			for (var i = 0; i < arg_list.length; i++) {
				var arg = arg_list[i];
				
				/*
				 * command:
				 * M — абсолютное смещение
				 * m — относительное смещение
				 */
				if (command == 'M' || pos == 0) {
					abs_x = arg[0];
					abs_y = arg[1];
				} else {
					abs_x += arg[0];
					abs_y += arg[1];
				}
				
				if (i == 0) {
					addCommand(new ictinus.drawItems.MoveTo(abs_x, abs_y));
				} else {
					// если несколько наборов аргументов, то все, кроме первого,
					// считаются как рисование линии в указанную точку
					addCommand(new ictinus.drawItems.LineTo(abs_x, abs_y));
				}
			}
		}
		
		function closepath(){
//			addCommand(ictinus.draw_type.close());
		}
		
		/**
		 * @param {String} command Команда
		 * @param {Array[]} arg_list Набор аргументов команды
		 * @param {Number} pos Порядковый номер команды в списке комманд
		 */
		function lineto(command, arg_list, pos) {
			arg_list = expandArgs(arg_list, 2);
			
			for (var i = 0; i < arg_list.length; i++) {
				var arg = arg_list[i];
				
				/*
				 * command:
				 * L — абсолютные координаты
				 * l — относительные координаты
				 */
				switch (command) {
					case 'L':
						abs_x = arg[0];
						abs_y = arg[1];
						break;
					case 'l':
						abs_x += arg[0];
						abs_y += arg[1];
						break;
				}
				
				addCommand(new ictinus.drawItems.LineTo(abs_x, abs_y));
			}
		}
		
		/**
		 * Горизонатльная или вертикальная линия
		 * @param {String} command Команда
		 * @param {Array[]} arg_list Набор аргументов команды
		 * @param {Number} pos Порядковый номер команды в списке комманд
		 */
		function hvlineto(command, arg_list, pos) {
			arg_list = expandArgs(arg_list, 1);
			
			for (var i = 0; i < arg_list.length; i++) {
				var arg = arg_list[i];
				
				/*
				 * command:
				 * H — горизонтальная линия, абсолютные координаты
				 * h — горизонтальная линия, относительные координаты
				 * V — вертикальная линия, абсолютные координаты
				 * v — вертикальная линия, относительные координаты
				 */
				switch (command) {
					case 'H':
						abs_x = arg[0];
						break;
					case 'h':
						abs_x += arg[0];
						break;
					case 'V':
						abs_y = arg[0];
						break;
					case 'v':
						abs_y += arg[0];
						break;
				}
				
				addCommand(new ictinus.drawItems.LineTo(abs_x, abs_y));
			}
		}
		
		/**
		 * @param {String} command Команда
		 * @param {Array[]} arg_list Набор аргументов команды
		 * @param {Number} pos Порядковый номер команды в списке комманд
		 */
		function curveto(command, arg_list, pos) {
			arg_list = expandArgs(arg_list, 6);
			for (var i = 0; i < arg_list.length; i++) {
				var arg = arg_list[i];
				
				var locked = (command.toLowerCase() == 'o');
				
				/*
				 * command:
				 * C — абсолютные координаты
				 * c — относительные координаты
				 */
				switch (command) {
					case 'C':
					case 'O':
						addCommand(new ictinus.drawItems.BezierTo(arg[4], arg[5], arg[0], arg[1], arg[2], arg[3], locked));
						last_cp = new Point(arg[2], arg[3]);
						
						abs_x = arg[4];
						abs_y = arg[5];
						break;
					case 'c':
					case 'o':
						addCommand(new ictinus.drawItems.BezierTo(abs_x + arg[4], abs_y + arg[5], abs_x + arg[0], abs_y + arg[1], abs_x + arg[2], abs_y + arg[3], locked));
						last_cp = new Point(abs_x + arg[2], abs_y + arg[3]);
						
						abs_x += arg[4];
						abs_y += arg[5];
						break;
				}
			}
		}
		
		/**
		 * @param {String} command Команда
		 * @param {Array[]} arg_list Набор аргументов команды
		 * @param {Number} pos Порядковый номер команды в списке комманд
		 */
		function scurveto(command, arg_list, pos) {
			arg_list = expandArgs(arg_list, 4);
			
			for (var i = 0; i < arg_list.length; i++) {
				var arg = arg_list[i];
				
				var tan_cp = ictinus.Shape.calculateTangentPoint(last_cp.x, last_cp.y, last_cp.x, last_cp.y, abs_x, abs_y);
				var locked = true;
				
				/*
				 * command:
				 * S — абсолютные координаты
				 * s — относительные координаты
				 */
				switch (command) {
					case 'S':
						addCommand(new ictinus.drawItems.BezierTo(arg[2], arg[3], tan_cp.x, tan_cp.y, arg[0], arg[1], locked));
						last_cp = new Point(arg[0], arg[1]);
						
						abs_x = arg[2];
						abs_y = arg[3];
						break;
					case 's':
						addCommand(new ictinus.drawItems.BezierTo(abs_x + arg[2], abs_y + arg[3], tan_cp.x, tan_cp.y, abs_x + arg[0], abs_y + arg[1], locked));
						last_cp = new Point(abs_x + arg[0], abs_y + arg[1]);
						
						abs_x += arg[2];
						abs_y += arg[3];
						break;
				}
				
			}
		}
		
		/**
		 * @param {String} command Команда
		 * @param {Array[]} arg_list Набор аргументов команды
		 * @param {Number} pos Порядковый номер команды в списке комманд
		 */
		function quadto(command, arg_list, pos) {
			
			arg_list = expandArgs(arg_list, 4);
			
			for (var i = 0; i < arg_list.length; i++) {
				var arg = arg_list[i];
				var c3;
				
				/*
				 * command:
				 * Q — абсолютные координаты
				 * q — относительные координаты
				 */
				switch (command) {
					case 'Q':
						c3 = conv2to3(abs_x, abs_y, arg[0], arg[1], arg[2], arg[3]);
						last_cp = new Point(arg[0], arg[1]);
						break;
					case 'q':
						c3 = conv2to3(abs_x, abs_y, abs_x + arg[0], abs_y + arg[1], abs_x + arg[2], abs_y + arg[3]);
						last_cp = new Point(abs_x + arg[0], abs_y + arg[1]);
						break;
				}
				
				addCommand(new ictinus.drawItems.BezierTo(c3.x, c3.y, c3.cp1x, c3.cp1y, c3.cp2x, c3.cp2y, false));
				
				abs_x = c3.x;
				abs_y = c3.y;
			}
		}
		
		/**
		 * @param {String} command Команда
		 * @param {Array[]} arg_list Набор аргументов команды
		 * @param {Number} pos Порядковый номер команды в списке комманд
		 */
		function squadto(command, arg_list, pos) {
			
			arg_list = expandArgs(arg_list, 2);
			
			for (var i = 0; i < arg_list.length; i++) {
				var arg = arg_list[i];
				
				var tan_cp = calculateTangentPoint(last_cp, new Point(abs_x, abs_y));
				var c3;
				
				/*
				 * command:
				 * T — абсолютные координаты
				 * t — относительные координаты
				 */
				switch (command) {
					case 'T':
						c3 = conv2to3(abs_x, abs_y, tan_cp.x, tan_cp.y, arg[0], arg[1]);
						break;
					case 't':
						c3 = conv2to3(abs_x, abs_y, tan_cp.x, tan_cp.y, abs_x + arg[0], abs_y + arg[1]);
						break;
				}
				
				addCommand(new ictinus.drawItems.BezierTo(c3.x, c3.y, c3.cp1x, c3.cp1y, c3.cp2x, c3.cp2y, false));
				last_cp = tan_cp;
				
				abs_x = arg[0];
				abs_y = arg[1];
			}
		}
		
		/**
		 * Рассчитывает координаты второй контрольной точки
		 * для обеспечения сохранности касательной между двумя кривыми Безье
		 * @param {Point} cp1 Точка, <i>относительно</i> которой нужно считать координаты
		 * @param {Point} anchor Концевая точка, которой принадлежат обе контрольные точки
		 * @return {Point}
		 */
		function calculateTangentPoint(cp1, anchor){
			var a = Math.atan2(cp1.y - anchor.y, cp1.x - anchor.x);
			var b = (a + Math.PI) % (Math.PI * 2);
		
			var catet_x = cp1.x - anchor.x;
			var catet_y = cp1.y - anchor.y;
		
			var hypot = Math.sqrt(catet_x * catet_x + catet_y * catet_y);
		
			return new Point(Math.cos(b) * hypot + anchor.x, Math.sin(b) * hypot + anchor.y);
		};
		
		/**
		 * Разбивает строку на набор аргументов.<br>
		 * Разные команды принимают разное количество аргументов. Например, 
		 * команда l (lineto) принимает 2 аргумента, а команда c (curveto) 
		 * принимает 3 аргумента. Стандарт SVG позволяет сцепить несолько 
		 * одинаковых, идущих подряд команд в одну, например: 
		 * l 1,2 l 3,4 можно записать как l 1,2,3,4. Соответственно, после 
		 * <code>parsePath()<code> аргументы команд могут содержать несколько 
		 * вызовов.<br><br> 
		 * 
		 * Текущий метод, во-первых, разбивает строку на массив значений, 
		 * а во-вторых группирует массив по количеству аргументов, необходимых
		 * для вызова команды (параметр <code>count</code>). В результате 
		 * получается массив массивов: первый уровень указывает, сколько всего
		 * вызовов комманды, а второй уровень содержит атрибуты комманды
		 * @param {String} args Аргументы, полученные в <code>parsePath()</code>
		 * @param {Number} count Количество аргументов, необходимых для вызова команды
		 * @return {Array[]}
		 */
		function expandArgs(args, count){
			// парсим только строки. если отдали что-то другое — ничего не делаем
			if (typeof args != 'string') {
				return args;
			}
			
			// сначала достаю все аргументы в один массив
			var re = /(\-?\d+(?:\.\d+)?)/g;
			var tmp, m = [];
			args = trim(args);
			
			while ((tmp = re.exec(args))) {
				m.push(tmp[1]);
			}
			
			// приведем все значения массива к числу
			for (var i = 0; i < m.length; i++) {
				m[i] = parseFloat(m[i], 10);
			}
			
			// теперь делим на группы
			var result = [];
			count = count || 1;
			for (var i = 0; i < m.length; i += count) {
				result.push(m.slice(i, i + count));
			}
			
			return result;
		}
		
		/** Карта команд преобразования */
		var command_map = {
			m: moveto,
			z: closepath,
			l: lineto,
			h: hvlineto,
			v: hvlineto,
			c: curveto,
			o: curveto,
			s: scurveto,
			q: quadto,
			t: squadto
		};
		
		return function(/* Array */ svg_commands) {
			abs_x = abs_y = 0;
			last_command = last_cp = null;
			
			commands = [];
			
			/*
			 * Начинаем преобразовывать команды. По мере исполнения сценария будет 
			 * автоматически заполняться массив commands, который затем возвращается 
			 * как результат
			 */
			for (var i = 0; i < svg_commands.length; i++) {
				var c = svg_commands[i];
				command_map[ c.command.toLowerCase() ](c.command, c.args, i);
			}
		
			return commands;
		}
	})();
	
	return {
		/**
		 * Парсит строку формата path data в набор команд для рисования 
		 * через ictinus
		 * @link http://www.w3.org/TR/SVG/paths.html#PathData
		 * @param {String} path Строка с path data
		 * @return {Array} Набор комманд для рисования
		 */
		parse: function(path){
			var str = trim(path);
			
			// удаляю все переводы строк
			str = str.replace(/\n|\r/g, ' ');
			
			var commands = [], 
				re = /([a-z])/ig, 
				last_ix = 0, 
				m, 
				last_command = null;
			
			function co(name, args){
				commands.push({
					command: name,
					args: trim(args)
				});
			}
			
			// теперь регулярным выражением буду находить буквы и записывать пары 
			// "команда-аргументы"
			while ((m = re.exec(str))) {
				if (last_command !== null) 
					co(
						last_command,
						str.substring(last_ix, re.lastIndex - last_command.length)
					);
				
				last_command = m[0];
				last_ix = re.lastIndex;
			}
			
			// добавляю самую последнюю команду (как правило, это z)
			co(last_command, str.substring(last_ix));
			
			return transformCommands(commands);
		}
	}
})();
